﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections.Generic;
using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.EventHandling;
using ScrumTable.Common.Monitoring;
using ScrumTable.Common.Threading;
using ScrumTable.DL.Data.Common;
using ScrumTable.DL.Data.Common.Config.Driver;
using ScrumTable.DL.Data.Common.Serialization;
using ScrumTable.DL.Data.Generic.Cache;
using ScrumTable.DL.Data.Generic.Properties;
using ScrumTable.DL.Data.Generic.Services;
using ScrumTable.DL.Data.Schema;
using ScrumTable.DL.Data.Schema.ScrumTableV1;

#endregion


namespace ScrumTable.DL.Data.Generic
{
    /// <summary>
    /// Contains all data used by the data layer, this is the entry
    /// point for the data layer and contains all required objects in
    /// order to connect to a data source.
    /// </summary>
    public abstract class GenericDataContextBase : DisposeBase, IDataContext
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private readonly FastSmartWeakEvent<UpdateBeginEventHandler> _beginUpdateEvents = new FastSmartWeakEvent<UpdateBeginEventHandler>();
        private readonly FastSmartWeakEvent<UpdateEndEventHandler> _endUpdateEvents = new FastSmartWeakEvent<UpdateEndEventHandler>();

        private readonly IDriverConfig _config;
        private DataStorage _storage;
        private DataConnectionBase _connection;
        private IDataSyncDispatcher _syncDispatcher;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Gets the driver information provided by the ScrumTable plugin
        /// framework.
        /// </summary>
        public IDataContextInfo DriverInformation { get; private set; }

        /// <summary>
        /// Gets associated data connection.
        /// </summary>
        public IDataConnection Connection { get { return _connection;} }

        /// <summary>
        /// Gets data storage.
        /// </summary>
        public IDataStorage Storage { get { return _storage;} }

        /// <summary>
        /// Gets associated driver configuration data.
        /// </summary>
        public IDriverConfig Configuration
        {
            get { return _config; }
        }

        /// <summary>
        /// Gets or sets the flow control dispatcher. This sychronization
        /// dispatcher is used for the async callback handling.
        /// </summary>
        /// <remarks>
        /// This dispatcher is in order to synchronize calls back into the main message queue.
        /// </remarks>
        public IDataSyncDispatcher DataSyncDispatcher
        {
            get { return _syncDispatcher; }
            set { _syncDispatcher = new SafeDataSyncDispatcher(value); }
        }

        /// <summary>
        /// Gets the associated data layer schema.
        /// </summary>
        public ScrumTableV1Schema Schema { get; private set; }

        /// <summary>
        /// Gets the work item store, stored in the Team Foundation Server.
        /// </summary>
        public CacheContext DataCache
        {
            get { return _connection.DataCache; }
        }

        /// <summary>
        /// Gets a flag which indicates that value change events on the
        /// current context should be propagated to the event listeners.
        /// </summary>
        public bool PropagateEvents
        {
            get { return DataCache.PropagateEvents; }
            set { DataCache.PropagateEvents = value; }
        }

        /// <summary>
        /// Gets associated data connection as fully qualified object.
        /// </summary>
        internal DataConnectionBase ConnectionInternal { get { return _connection; } }

        /// <summary>
        /// Gets data storage  as fully qualified object.
        /// </summary>
        internal DataStorage StorageInternal { get { return _storage; } }

        /// <summary>
        /// Gets the internal data loader instance.
        /// </summary>
        protected internal abstract IDataLoader DataLoader { get;  }

        /// <summary>
        /// Gets the internal data saver instance.
        /// </summary>
        protected internal abstract IDataSaver DataSaver { get; }

        /// <summary>
        /// Gets the data save manager factory which is used in conjunction
        /// with the connect procedure.
        /// </summary>
        protected internal abstract IDataSaveManagerFactory DataSaveManagerFactory { get; }

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new DataContext instance. This constructor must be
        /// public because it's being created by the ScrumTable base library.
        /// </summary>
        protected GenericDataContextBase(IDataContextInfo driverInfo)
        {
            PreCondition.AssertNotNull(driverInfo, "driverInfo");
            PreCondition.AssertNotNull(driverInfo.ConfiguredSchemaSource, "schemaSource");
            PreCondition.AssertNotNull(driverInfo.ConfiguredDriverConfigSource, "driverConfigSource");

            DriverInformation = driverInfo;
            DataSyncDispatcher = new SyncDataDispatcher();
            Schema = SchemaFactory.CreateSchema(DriverInformation.ConfiguredSchemaSource.SourceData);
            
            _storage = new DataStorage(this);
            _connection = CreateDataConnection();
            _config = DriverInformation.ConfiguredDriverConfigSource.SourceData;
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Stores the data of the current data context to the given
        /// location.
        /// </summary>
        /// <param name="fileSystemPath">File system path where the xml file is located.</param>
        public void SerializeTo(string fileSystemPath)
        {
            EnsureNotDisposed();
            new DataStorageSerializer(Schema).Serialize(
                fileSystemPath,
                Storage,
                new DataResolver(ConnectionInternal.ConnectionManager.Selected));
        }

        /// <summary>
        /// Stores the data of the current data context to the given moniker
        /// file path.
        /// </summary>
        public void Serialize()
        {
            SerializeTo(Connection.ConnectionMoniker);
        }

        /// <summary>
        /// Loads all data of the current data context below the given id from the given
        /// location.
        /// </summary>
        /// <param name="idChain">The data below the given id should be imported.</param>
        /// <param name="importMonitor">Specifies the monitor which observes the import.</param>
        public void ImportData(ValueChain<string> idChain, IDataImportMonitor importMonitor)
        {
            EnsureNotDisposed();

            DataLoader.LoadInitialData(
                this,
                idChain,
                new DataImportMonitorSync(DataSyncDispatcher,
                    new CallbackDataImportMonitor(LoadDataCompletedSync,
                        new RangeImportMonitor(
                            new TimeLoggerImportMonitor(importMonitor)))));
        }

        /// <summary>
        /// Stores the data of the current data context back to the underlying system.
        /// </summary>
        /// <param name="callbackEvent">Specifies the callback which is fired after the
        /// process has been completed. The callback is of type ICancellableDataResult
        /// and its Data property contains true, if all changes have been accepted.</param>
        public void ExportData(AsyncCallback callbackEvent)
        {
            if (ConnectionInternal.DataCache.CurrentProject == null)
            {
                throw new InvalidOperationException(Resources.ExcDataCtxExportDataFailed);
            }

            ConnectionInternal.SaveManager.Stop(callbackEvent);
        }

        /// <summary>
        /// Creates the new data connection instance which is used to connect to the
        /// underlying data storage.
        /// </summary>
        /// <returns>Returns the newly created instance.</returns>
        protected abstract DataConnectionBase CreateDataConnection();

        /// <summary>
        /// Releases managed (c# or .net) resources.
        /// </summary>
        protected override void ReleaseManagedResources()
        {
            if (_connection != null)
            {
                _connection.Dispose();
                _storage.Dispose();
                _storage = null;
                _connection = null;
            }
        }

        /// <summary>
        /// Checks the cache data structures for changes and lists them
        /// in the CacheDataChanges instance.
        /// </summary>
        /// <returns>Returns the changes as a new instance.</returns>
        public CacheDataChanges ExportChanges()
        {
            if (Connection.IsConnected)
            {
                return DataCache.GetCurrentChanges();
            }
            return new CacheDataChanges();
        }

        /// <summary>
        /// Merges the specified changes with into the cache. 
        /// </summary>
        /// <param name="toAccept">Specifies the changed entries which have been processed.</param>
        public void ImportChanges(CacheDataChanges toAccept)
        {
            PreCondition.AssertNotNull(toAccept, "toAccept");

            if (Connection.IsConnected)
            {
                using (StartDataImport(toAccept))
                {
                    DataCache.MergeCurrentChanges(toAccept);
                }
            }
        }

        /// <summary>
        /// Starts a data import on the data cache. Calling this method
        /// will notify all layers above about the data mutation.
        /// </summary>
        /// <returns>Returns a disposable object which can be used to
        /// release the bulk update state.</returns>
        private IDisposable StartDataImport(CacheDataChanges toAccept)
        {
            OnBeginUpdate(EventArgs.Empty);
            return DataCache.EnableEventPropagation(() => OnEndUpdate(new UpdateEndEventArgs(GetErrorPropagationData(toAccept))));
        }

        private IList<IDataSyncError> GetErrorPropagationData(CacheDataChanges toAccept)
        {
            IList<IDataSyncError> errorData = new List<IDataSyncError>(toAccept.WorkItemsToSync.Count);

            foreach (var syncResult in toAccept.WorkItemsToSync)
            {
                if (syncResult.Value.HasError)
                {
                    ValueChain<string> workItemChain = DataCache.CurrentProject.WorkItems.TryGetChainById(syncResult.Value.Bucket.Element.Id);
                    IList<IDataSyncErrorInfo> saveErrors = syncResult.Value.Errors.Convert<IDataSyncErrorInfo, CacheSaveError>(ce => new DataSyncErrorInfo(ce));

                    errorData.Add(new DataSyncError(workItemChain, saveErrors));
                }
            }
            return errorData;
        }

        private void LoadDataCompletedSync(IAsyncResult result)
        {
            if (!((DataResult)result).HasError)
            {
                StorageInternal.OnInitialized();
                ConnectionInternal.SaveManager.Start();
                
                // INCLUDE THIS LINE IF YOU D'LIKE TO EXPORT A SET OF DATA
                //SerializeTo("C:\\Temp\\data.out");
            }
        }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        /// <summary>
        /// Specifies that the bulk data update starts.
        /// </summary>
        public event UpdateBeginEventHandler BeginUpdate
        {
            add { _beginUpdateEvents.Add(value); }
            remove { _beginUpdateEvents.Remove(value); }
        }

        /// <summary>
        /// Specifies that the bulk data update ends.
        /// </summary>
        public event UpdateEndEventHandler EndUpdate
        {
            add { _endUpdateEvents.Add(value); }
            remove { _endUpdateEvents.Remove(value); }            
        }

        /// <summary>
        /// Fires the BeginUpdate event.
        /// </summary>
        /// <param name="e">Specifies the event args to fire to the clients.</param>
        protected virtual void OnBeginUpdate(EventArgs e)
        {
            _beginUpdateEvents.Raise(this, e);
        }

        /// <summary>
        /// Fires the EndUpdate event.
        /// </summary>
        /// <param name="e">Specifies the event args to fire to the clients.</param>
        protected virtual void OnEndUpdate(UpdateEndEventArgs e)
        {
            _endUpdateEvents.Raise(this, e);
        }

        #endregion
    }
}